
/*
 * Copyright:
 * Daniel D. Neilson (ddneilson@ieee.org)
 * University of Saskatchewan
 * All rights reserved
 * 
 * Extended by:
 * Ben Schmidt - bjs778 - 11054018
 * Mike Nimeck - mfn028 - 11053045
 * Jeff Huang - jlh105 - 11050226
 *
 * Permission granted to use for use in assignments and
 * projects for CMPT 485 & CMPT 829 at the University
 * of Saskatchewan.
 */

#include <GLTools.h>
#include <GLMatrixStack.h>
#include <GLFrame.h>
#include <GLFrustum.h>
#include <GLGeometryTransform.h>
#include <StopWatch.h>
#include <GL/glut.h>
#include <cstdio>
#include <cstdlib>
#include <cstring>
#include <png.h>

#include "glUtils.h"
#include "main.h"

static const char *getErrorString(GLenum err)
{
	switch (err)
	{
	case GL_NO_ERROR: return "No error";
	case GL_INVALID_ENUM: return "invalid enum";
	case GL_INVALID_VALUE: return "invalid value";
	case GL_INVALID_OPERATION: return "invalid operation";
	case GL_OUT_OF_MEMORY: return "out of memory";
	case GL_INVALID_FRAMEBUFFER_OPERATION: return "invalid framebuffer operation";
	default:
		break;
	}
	return "unknown error";
}

#define EXIT_ON_ERROR
bool _isGLError(const char *file, const int line)
{
	GLenum err = glGetError();
	if (err != GL_NO_ERROR)
	{
		fprintf(stderr, "GL ERROR: File '%s' at line %d: %s\n",
						file, line, getErrorString(err));
#if defined(EXIT_ON_ERROR)
		exit(1);
#endif
		return true;
	}
	return false;
}

bool isExtensionSupported(const char *name)
{
	GLint n=0;
	glGetIntegerv(GL_NUM_EXTENSIONS, &n);
	for (GLint i=0; i<n; i++)
	{
		const char* extension =
				(const char*)glGetStringi(GL_EXTENSIONS, i);
		if (!strcmp(name, extension))
		{
			return true;
		}
	}
	return false;
}

bool compileShader(const GLuint handle)
{
	if (handle == 0)
	{
		return false;
	}
	glCompileShader(handle);

	// Check for errors
	GLint success;
	glGetShaderiv(handle, GL_COMPILE_STATUS, &success);
	if (success == GL_FALSE)
	{
		char errLog[1024];
		glGetShaderInfoLog(handle, 1024, NULL, errLog);
		fprintf(stderr,
				"========== COMPILE ERROR ===================\n%s\n"
				"=================================\n\n=================================\n",
				errLog);
		return false;
	}
	return true;
}

//makes sure the shader program linked correctly and is valid
bool validateShader(GLuint render_program){
    // Make sure the link was successful
    GLint success;
    glGetProgramiv(render_program, GL_LINK_STATUS, &success);
    if (success == GL_FALSE)
    {
	    char errLog[1024];
	    glGetProgramInfoLog(success, 1024, NULL, errLog);
	    fprintf(stderr,
			    "========== LINK ERROR ===================\n%s\n",
			    errLog);
	    glDeleteProgram(render_program);
	    render_program = 0;
	    return false;
    }

    // Validate the linked program
    glValidateProgram(render_program);
    glGetProgramiv(render_program, GL_VALIDATE_STATUS, &success);
    if (!success) {
	    char errLog[1024];
	    glGetProgramInfoLog(render_program, sizeof(errLog), NULL, errLog);
	    fprintf(stderr, "========== VALIDATE ERROR ===================\n%s\n", errLog);
	    glDeleteProgram(render_program);
	    render_program = 0;
	    return false;
    }
    
    return true;
}


// Load a TGA as a 2D Texture. Completely initialize the state
// shamelessly stolen from the OpenGL SuperBible
bool LoadTGATexture(const char *szFileName, GLenum minFilter, GLenum magFilter, GLenum wrapMode)
{
        GLbyte *pBits;
        int nWidth, nHeight, nComponents;
        GLenum eFormat;
        
        // Read the texture bits
        pBits = gltReadTGABits(szFileName, &nWidth, &nHeight, &nComponents, &eFormat);
        if(pBits == NULL) 
                return false;
        
        glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_WRAP_S, wrapMode);
        glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_WRAP_T, wrapMode);
        
        glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_MIN_FILTER, minFilter);
        glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_MAG_FILTER, magFilter);
    
        glPixelStorei(GL_UNPACK_ALIGNMENT, 1);
        glTexImage2D(GL_TEXTURE_2D, 0, nComponents, nWidth, nHeight, 0,
                                 eFormat, GL_UNSIGNED_BYTE, pBits);
        
    free(pBits);
    
    if(minFilter == GL_LINEAR_MIPMAP_LINEAR || 
       minFilter == GL_LINEAR_MIPMAP_NEAREST ||
       minFilter == GL_NEAREST_MIPMAP_LINEAR ||
       minFilter == GL_NEAREST_MIPMAP_NEAREST)
        glGenerateMipmap(GL_TEXTURE_2D);
    
        return true;
}

//adapted from LoadTGATexture
bool LoadPNGTexture(const char *szFileName, GLenum minFilter, GLenum magFilter, GLenum wrapMode)
{
        GLubyte *pBits;
        int nWidth, nHeight, nComponents;
        GLenum eFormat;
        bool hasAlpha;

	// Read the texture bits
	if (!loadPng(szFileName, nWidth, nHeight, hasAlpha, &pBits)) {
	    fprintf(stderr, "Unable to load png file\n");
	    return false;
	}

        glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_WRAP_S, wrapMode);
        glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_WRAP_T, wrapMode);
        
        glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_MIN_FILTER, minFilter);
        glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_MAG_FILTER, magFilter);
    
        glPixelStorei(GL_UNPACK_ALIGNMENT, 1);
	glTexImage2D(GL_TEXTURE_2D, 0, hasAlpha ? 4 : 3, nWidth,
	            nHeight, 0, hasAlpha ? GL_RGBA : GL_RGB, GL_UNSIGNED_BYTE,
	            pBits);
        
    free(pBits);
    
    if(minFilter == GL_LINEAR_MIPMAP_LINEAR || 
       minFilter == GL_LINEAR_MIPMAP_NEAREST ||
       minFilter == GL_NEAREST_MIPMAP_LINEAR ||
       minFilter == GL_NEAREST_MIPMAP_NEAREST)
        glGenerateMipmap(GL_TEXTURE_2D);
    
        return true;
}

//loads a png
//stolen shamelessly from the internet
bool loadPng(const char *name, int &outWidth, int &outHeight, bool &outHasAlpha, GLubyte **outData) {
    png_structp png_ptr;
    png_infop info_ptr;
    unsigned int sig_read = 0;
    int color_type, interlace_type;
    FILE *fp;
    
    if ((fp = fopen(name, "rb")) == NULL)
	return false;
    
    /* Create and initialize the png_struct
	* with the desired error handler
	* functions.  If you want to use the
	* default stderr and longjump method,
	* you can supply NULL for the last
	* three parameters.  We also supply the
	* the compiler header file version, so
	* that we know if the application
	* was compiled with a compatible version
	* of the library.  REQUIRED
	*/
    png_ptr = png_create_read_struct(PNG_LIBPNG_VER_STRING,
	    NULL, NULL, NULL);
    
    if (png_ptr == NULL) {
	fclose(fp);
	return false;
    }
    
    /* Allocate/initialize the memory
	* for image information.  REQUIRED. */
    info_ptr = png_create_info_struct(png_ptr);
    if (info_ptr == NULL) {
	fclose(fp);
	png_destroy_read_struct(&png_ptr, png_infopp_NULL, png_infopp_NULL);
	return false;
    }
    
    /* Set error handling if you are
	* using the setjmp/longjmp method
	* (this is the normal method of
	* doing things with libpng).
	* REQUIRED unless you  set up
	* your own error handlers in
	* the png_create_read_struct()
	* earlier.
	*/
    if (setjmp(png_jmpbuf(png_ptr))) {
	/* Free all of the memory associated
	    * with the png_ptr and info_ptr */
	png_destroy_read_struct(&png_ptr, &info_ptr, png_infopp_NULL);
	fclose(fp);
	/* If we get here, we had a
	    * problem reading the file */
	return false;
    }
    
    /* Set up the output control if
	* you are using standard C streams */
    png_init_io(png_ptr, fp);
    
    /* If we have already
	* read some of the signature */
    png_set_sig_bytes(png_ptr, sig_read);
    
    /*
	* If you have enough memory to read
	* in the entire image at once, and
	* you need to specify only
	* transforms that can be controlled
	* with one of the PNG_TRANSFORM_*
	* bits (this presently excludes
	* dithering, filling, setting
	* background, and doing gamma
	* adjustment), then you can read the
	* entire image (including pixels)
	* into the info structure with this
	* call
	*/
    png_read_png(png_ptr, info_ptr, PNG_TRANSFORM_IDENTITY, png_voidp_NULL);
    
    outWidth = info_ptr->width;
    outHeight = info_ptr->height;
    switch (info_ptr->color_type) {
	case PNG_COLOR_TYPE_RGBA:
	    outHasAlpha = true;
	    break;
	case PNG_COLOR_TYPE_RGB:
	    outHasAlpha = false;
	    break;
	default:
	    fprintf(stderr, "Color type %s not supported\n", info_ptr->color_type);
	    png_destroy_read_struct(&png_ptr, &info_ptr, NULL);
	    fclose(fp);
	    return false;
    }
    unsigned int row_bytes = png_get_rowbytes(png_ptr, info_ptr);
    *outData = (unsigned char*) malloc(row_bytes * outHeight);
    
    png_bytepp row_pointers = png_get_rows(png_ptr, info_ptr);
    
    for (int i = 0; i < outHeight; i++) {
	// note that png is ordered top to
	// bottom, but OpenGL expect it bottom to top
	// so the order or swapped
	memcpy(*outData+(row_bytes * (outHeight-1-i)), row_pointers[i], row_bytes);
    }
    
    /* Clean up after the read,
	* and free any memory allocated */
    png_destroy_read_struct(&png_ptr, &info_ptr, png_infopp_NULL);
    
    /* Close the file */
    fclose(fp);
    
    /* That's it */
    return true;
}

//load in the current shader program
void loadShaderProgram(){
    //
    //create the render program
    //
    render_program = glCreateProgram();

    //load and compile the vertex shader
    render_vs = glCreateShader(GL_VERTEX_SHADER);
    gltLoadShaderFile(VERTEX_SHADERS[SHADER_INDEX], render_vs);
    compileShader(render_vs);
    glAttachShader(render_program, render_vs);
    
    //load and compile the geometry shader, if it exists
    if (GEOMETRY_SHADERS[SHADER_INDEX] != NULL)
    {
	    render_gs = glCreateShader(GL_GEOMETRY_SHADER);
	    gltLoadShaderFile(GEOMETRY_SHADERS[SHADER_INDEX], render_gs);
	    compileShader(render_gs);
	    glAttachShader(render_program, render_gs);
    }
    
    //load and compile the fragment shader
    render_fs = glCreateShader(GL_FRAGMENT_SHADER);
    gltLoadShaderFile(FRAGMENT_SHADERS[SHADER_INDEX], render_fs);
    compileShader(render_fs);
    glAttachShader(render_program, render_fs);

    //link the render program
    glLinkProgram(render_program);
    
    //make sure things linked correctly, and that the shader program is valid
    if (!validateShader(render_program))
    {
	    return;
    }
    
    glUseProgram(render_program);
    
    //load the texture
    texture_loc = glGetUniformLocation(render_program, "sprite");
    glGenTextures(1, &particle_sprite);
    glBindTexture(GL_TEXTURE_2D, particle_sprite);
    
    if (SHADER_INDEX == LSD_SHADER || SHADER_INDEX == TECHNOBABBLE_SHADER){
	LoadTGATexture("sprite.tga", 
	    GL_LINEAR_MIPMAP_LINEAR, GL_LINEAR, GL_CLAMP_TO_EDGE);
    }
    else{
	LoadPNGTexture("sprite.png", 
	    GL_LINEAR_MIPMAP_LINEAR, GL_LINEAR, GL_CLAMP_TO_EDGE);	
    }
    
    //set the uniforms
    mvp_loc = glGetUniformLocation(render_program, "mvp");
    
    base_size_loc = glGetUniformLocation(render_program, "base_size");
    depth_divisor_loc = glGetUniformLocation(render_program, "depth_divisor");
    glUniform1i(base_size_loc, base_size);
    glUniform1f(depth_divisor_loc, depth_divisor);
    
    if (SHADER_INDEX == AIRPLANES_SHADER){
	printf("AIRPLANES shader loaded.\n");
    }
    else if (SHADER_INDEX == POINTILISM_SHADER){
	printf("POINTILISM shader loaded.\n");
    }
    else if (SHADER_INDEX == VELOCITY_SHADER){
	printf("VELOCITY shader loaded.\n");
    }
    else if (SHADER_INDEX == LSD_SHADER){
	printf("LSD shader loaded.\n");
    }
    else if (SHADER_INDEX == TECHNOBABBLE_SHADER){
	printf("TECHNOBABBLE shader loaded.\n");
    }
    else if (SHADER_INDEX == BUBBLES_SHADER){
	printf("BUBBLES shader loaded.\n");
    }
    else if (SHADER_INDEX == POSITION_SHADER){
	printf("POSITION shader loaded.\n");
    }
    else if (SHADER_INDEX == SHALLOW_SHADER){
	printf("SHALLOW shader loaded.\n");
    }
    else if (SHADER_INDEX == COLORFLOCK_SHADER){
	printf("COLOR FLOCKING shader loaded.\n");
    }
    else if (SHADER_INDEX == GRAVMASS_SHADER){
	printf("MASS shader loaded.\n");
    }
    else if (SHADER_INDEX == RAINBOW_SHADER){
	printf("RAINBOW shader loaded.\n");
    }
    else if (SHADER_INDEX == FIREWORKS_SHADER){
	printf("FIREWORKS shader loaded.\n");
    }
}

void incrementShader(){
    SHADER_INDEX++;
    
    if (particle_type != FLOCKING && SHADER_INDEX == COLORFLOCK_SHADER){
	SHADER_INDEX++;
    }
    
    if (particle_type != GRAVITY && SHADER_INDEX == GRAVMASS_SHADER){
	SHADER_INDEX++;
    }
    
    if (particle_type != FIREWORKS && SHADER_INDEX == FIREWORKS_SHADER){
	SHADER_INDEX++;
    }
    
    if (SHADER_INDEX >= SHADER_COUNT)
    {
	    SHADER_INDEX = 0;
    }
    
    if (particle_type != FLOCKING && SHADER_INDEX == COLORFLOCK_SHADER){
	SHADER_INDEX++;
    }
    
    if (particle_type != GRAVITY && SHADER_INDEX == GRAVMASS_SHADER){
	SHADER_INDEX++;
    }
   
    if (particle_type != FIREWORKS && SHADER_INDEX == FIREWORKS_SHADER){
	SHADER_INDEX++;
    }
    
    loadShaderProgram();

}

void decrementShader(){
    SHADER_INDEX--;
    
    if (particle_type != FIREWORKS && SHADER_INDEX == FIREWORKS_SHADER){
	SHADER_INDEX--;
    }
    
    if (particle_type != GRAVITY && SHADER_INDEX == GRAVMASS_SHADER){
	SHADER_INDEX--;
    }
    
    if (particle_type != FLOCKING && SHADER_INDEX == COLORFLOCK_SHADER){
	SHADER_INDEX--;
    }
    
    if (SHADER_INDEX < 0)
    {
	    SHADER_INDEX = SHADER_COUNT - 1;
    }
    
    if (particle_type != FIREWORKS && SHADER_INDEX == FIREWORKS_SHADER){
	SHADER_INDEX--;
    }
    
    if (particle_type != GRAVITY && SHADER_INDEX == GRAVMASS_SHADER){
	SHADER_INDEX--;
    }
    
    if (particle_type != FLOCKING && SHADER_INDEX == COLORFLOCK_SHADER){
	SHADER_INDEX--;
    }
    
    loadShaderProgram();
}

void incrementTrailLength(){
    if (trail_length >= 1.0f){
	trail_length = 1.0f;
    }
    if (trail_length < 0.7f){
	trail_length += 0.1f;
    }
    else if (trail_length < 0.95f){
	trail_length += 0.05f;
    }
    else if (trail_length < 0.99f){
	trail_length += 0.01f;
    }
    else if (trail_length < 1.0f){
	trail_length += 0.001f;
    }
    
    if (trail_length > 1.0f){
	trail_length = 1.0f;
    }
    
    printf("Trail Length: %f\n", trail_length);
}

void decrementTrailLength(){
    if (trail_length <= 0.0f){
	trail_length = 0.0f;
    }
    else if (trail_length <= 0.7f){
	trail_length -= 0.1f;
    }
    else if (trail_length <= 0.95f){
	trail_length -= 0.05f;
    }
    else if (trail_length <= 0.99f){
	trail_length -= 0.01f;
    }
    else if (trail_length <= 1.0f){
	trail_length -= 0.001f;
    }
    
    if (trail_length < 0.0f){
	trail_length = 0.0f;
    }
    
    printf("Trail Length: %f\n", trail_length);
}

void incrementDepthDivisor(){
    if (depth_divisor >= 25){
	depth_divisor += 5.0f;
    }
    else if (depth_divisor >= 5){
	depth_divisor++;
    }
    else if (depth_divisor >= 2.5){
	depth_divisor += 0.5f;
    }
    else if (depth_divisor >= 0.1f){
	depth_divisor += 0.1f;
    }
    
    glUniform1f(depth_divisor_loc, depth_divisor);
    printf("Depth Divisor: %f\n", depth_divisor);
}

void decrementDepthDivisor(){
    if (depth_divisor > 25){
	depth_divisor -= 5.0f;
    }
    else if (depth_divisor > 5){
	depth_divisor--;
    }
    else if (depth_divisor > 2.5){
	depth_divisor -= 0.5f;
    }
    else if (depth_divisor > 0.1f){
	depth_divisor -= 0.1f;
    }
    
    if (depth_divisor < 0.1f){
	depth_divisor = 0.1f;
    }
	
    glUniform1f(depth_divisor_loc, depth_divisor);
    printf("Depth Divisor: %f\n", depth_divisor);
}
